﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web.Security;
using System.Web.SessionState;
using System.Web.Routing;
using System.Reflection;
using SimpleRest.Core.Routing;
using SimpleRest.Core.Exceptions;
using SimpleRest.Core.Routing.Handlers;

namespace SimpleRest.Core
{
    /// <summary>
    /// The REST pipeline implementation.  This is where the pipeline logic gets executed.
    /// </summary>
    public sealed class Pipeline
    {
        private static List<Type> PrimitiveTypes = new List<Type>()
            {
                typeof(Int16),
                typeof(Int32),
                typeof(Int64),
                typeof(double),
                typeof(float),
                typeof(char),
                typeof(Guid),
                typeof(string),
                typeof(byte),
                typeof(UInt16),
                typeof(UInt32),
                typeof(UInt64),
                typeof(DateTime),
                typeof(bool),
                typeof(decimal)
            };

        /// <summary>
        /// The associated service type for this pipeline.
        /// </summary>
        public readonly Type ServiceType;
        /// <summary>
        /// The associates service attribute for the service this pipline is associated to.
        /// </summary>
        private readonly ServiceAttribute ServiceAttribute;
        /// <summary>
        /// In the event the service was registered extending ServiceExtension this field will be non-null and point to a strongly typed
        /// ServiceExtension class.
        /// </summary>
        private readonly ServiceBase SpecifiedServiceExtension;

        public Pipeline(object service)
            : this(service == null ? null : service.GetType())
        {
            if (service == null)
                throw new Exception("Unable to execute request pipeline with a null service reference.");
            this.ServiceType = service.GetType();

            if (service is ServiceBase)
                this.SpecifiedServiceExtension = service as ServiceBase;
        }
        public Pipeline(Type serviceType)
        {
            if (serviceType == null)
                throw new Exception("Unable to execute request pipeline with a null service reference.");

            this.ServiceType = serviceType;

            foreach (var attr in serviceType.GetCustomAttributes(true))
            {
                if (attr is ServiceAttribute)
                {
                    this.ServiceAttribute = attr as ServiceAttribute;
                    break;
                }
            }

            if (this.ServiceAttribute == null)
                throw new Exception("Unable to execute request pipeline for non-attributed service.  A service attribute must be specified.");
        }

        /// <summary>
        /// Execute request against pipeline targeting a particular operation.
        /// </summary>
        /// <param name="request">REST request.</param>
        /// <param name="routeInformation">Associated route information needed to perform mappings.</param>
        /// <param name="configuration">Configuration to execute against this operation request.</param>
        /// <returns></returns>
        public IHttpResponseMessage ExecuteOperation(IHttpRequestMessage request, Configuration configuration)
        {
            return this.ExecuteOperation(RouteUtility.GetOperationMethod(this.ServiceType, request.RouteInformation.RouteUrl, request.Uri, request.Verb), request, configuration);
        }

        public IEnumerable<ExecuteResponseHandler> ExecuteMessageHandlers(MessageHandlerContext requestContext, Configuration configuration)
        {
            List<ExecuteResponseHandler> servicesResponses = new List<ExecuteResponseHandler>();

            if (configuration != null && configuration.MessageHandlers != null)
                foreach (var messageHandler in configuration.MessageHandlers)
                    servicesResponses.Add(messageHandler.Execute(requestContext));

            return servicesResponses;
        }
        public void ExecuteResponseHandlers(IEnumerable<ExecuteResponseHandler> responseHandlers, IHttpResponseMessage response)
        {
            foreach (var item in responseHandlers)
            {
                if (item == null)
                    continue;
                item.Invoke(response);
            }
        }

        /// <summary>
        /// Execute request against pipeline targeting a particular operation.  This method takes the actual method to be executed so that no method determining needs
        /// to happen to perform operation request saving time.
        /// </summary>
        /// <param name="request">REST request.</param>
        /// <param name="targetMethod">Specify the exact method.</param>
        /// <param name="routeInformation">Associated route information needed to perform mappings.</param>
        /// <param name="configuration">Configuration to execute against this operation request.</param>
        /// <returns></returns>
        internal IHttpResponseMessage ExecuteOperation(MethodInfo targetMethod, IHttpRequestMessage request, Configuration configuration)
        {
            // if (string.IsNullOrEmpty(methodName))
            //    throw new Exception("A valid service operation name must be specified.");
            if (request == null)
                throw new Exception("A valid HttpRequestMessage must be specified.");
            if (configuration == null)
                throw new Exception("A valid Configuration must be specified.");

            IHttpResponseMessage httpResponseMessage = new HttpResponseMessage();
            try
            {
                if (targetMethod == null)
                    throw new Exception("A valid service operation must be specified.");

                object result = null;
                object classInstance = null;
                ServiceBase serviceExtension = this.SpecifiedServiceExtension;

                Operation targetOperation = RouteUtility.GetOperation(RouteUtility.GetRoutePrefix(this.ServiceType), targetMethod);
                var requestFormatter = GetRequestMediaTypeFormatter(request, configuration);

                var httpBodyOperation = targetOperation.Parameters.FirstOrDefault(d => d.IsHttpBody);
                    Type httpBodyType = null;
                    if (httpBodyOperation != null)
                        httpBodyType = httpBodyOperation.Type;

                    if (requestFormatter == null || !requestFormatter.CanDeserialize(httpBodyType))
                    {
                        httpResponseMessage.StatusCode = System.Net.HttpStatusCode.UnsupportedMediaType;
                        return httpResponseMessage;
                    }

                var responseFormatter = GetResponseMediaTypeFormatter(request, configuration);
               
                    if (responseFormatter == null || !responseFormatter.CanSerialize(targetOperation.ReturnType))
                    {
                        httpResponseMessage.StatusCode = System.Net.HttpStatusCode.UnsupportedMediaType;
                        return httpResponseMessage;
                    }

                var servicesResponses = this.ExecuteMessageHandlers(new MessageHandlerContext(request, this.ServiceType, targetOperation), configuration);

                if (this.SpecifiedServiceExtension == null)
                {
                    try
                    {
                        classInstance = RouteUtility.CreateInstance(this.ServiceType, request, configuration.CreateServiceInstance);

                        if (classInstance == null)
                            throw new Exception("Unable to create a non-null instance of service associated to request.");

                        if (classInstance is ServiceBase)
                            serviceExtension = classInstance as ServiceBase;
                    }
                    catch (Exception ex)
                    {
                        throw new ObjectActivationException(this.ServiceType, ex);
                    }
                }
                else
                    classInstance = this.SpecifiedServiceExtension;

                if (serviceExtension != null)
                {
                    serviceExtension._request = request;
                    serviceExtension._registeredOperations = RouteUtility.GetOperations(this.ServiceType);
                }

                try
                {
                    result = this.InvokeOperation(classInstance, request, targetMethod, requestFormatter, targetOperation);
                }
                catch (Exception ex)
                {
                    try
                    {
                        if (serviceExtension != null) serviceExtension.OperationExecutionException(targetOperation, ex);
                    }
                    catch (Exception serviceExtException)
                    {
                        throw;
                    }
                    throw;
                }

                if (serviceExtension != null) serviceExtension.OperationPostExecution(targetOperation, result);

                if (result is IOperationResponse)
                {
                    var response = result as IOperationResponse;
                    httpResponseMessage.Content = response.Content;
                    httpResponseMessage.StatusCode = response.StatusCode;

                    if (!string.IsNullOrEmpty(response.RedirectUrl))
                        httpResponseMessage.ResponseHeaders.Add(new Header("Location", response.RedirectUrl));
                }
                else
                {
                    httpResponseMessage.Content = result;
                    httpResponseMessage.StatusCode = System.Net.HttpStatusCode.OK;
                }

                this.ExecuteResponseHandlers(servicesResponses, httpResponseMessage);

                return httpResponseMessage;
            }
            catch (Exception ex)
            {
                httpResponseMessage.StatusCode = System.Net.HttpStatusCode.BadRequest;
                configuration.ErrorHandler(new ErrorHandlerEventArgs(ex, this.ServiceType, request, httpResponseMessage));
            }

            return httpResponseMessage;
        }

        /// <summary>
        /// Perform operation input mappings and execute service operation.
        /// </summary>
        /// <param name="serviceInstance"></param>
        /// <param name="httpRequestMessage"></param>
        /// <param name="routeInformation"></param>
        /// <param name="method"></param>
        /// <param name="formatter"></param>
        /// <param name="operation"></param>
        /// <returns></returns>
        private object InvokeOperation(object serviceInstance, IHttpRequestMessage httpRequestMessage, MethodInfo method, IMediaTypeFormatter formatter, Operation operation)
        {
            List<KeyValuePair<ParameterInfo, object>> orderedItems = new List<KeyValuePair<ParameterInfo, object>>();
            object body = null;
            object paramValue = null;
            Dictionary<string, IOperationMappingHandler> OperationMappingHandlers = new Dictionary<string, IOperationMappingHandler>(StringComparer.OrdinalIgnoreCase);

            if (serviceInstance is ServiceBase)
            {
                ServiceBase extension = serviceInstance as ServiceBase;
                if (extension._mappings != null)
                {
                    foreach (var mapping in extension._mappings)
                    {
                        if (mapping != null)
                        {
                            OperationMappingHandlers.Add(mapping.OutputParameterName, mapping);
                        }
                    }
                }
            }

            foreach (var param in method.GetParameters())
            {
                bool isFound = false;
                var httpBodyAttribute = GetAttribute<HttpBodyAttribute>(param);

                if (httpBodyAttribute != null)
                {
                    if (body != null)
                        throw new Exception("There were multiple http body attributes found for this operation.");
                    if (OperationMappingHandlers.ContainsKey(param.Name))
                        throw new Exception("A mapped input parameter can not also serve as the http body input.");
                    //System.IO.StreamReader sr = new System.IO.StreamReader(httpRequestMessage.Content);
                    // string requestText = sr.ReadToEnd();

                    try
                    {
                        body = formatter.DeSerialize(httpRequestMessage.Content, param.ParameterType);
                    }
                    catch (Exception ex)
                    {
                        throw new ContentBodyMalformedException();
                    }

                    RouteUtility.raiseHttpRequestBodyResolved(this.ServiceType, operation, body);

                    orderedItems.Add(new KeyValuePair<ParameterInfo, object>(param, body));

                    isFound = true;
                    // attempt to deserialize object from body.
                }
                else if (OperationMappingHandlers.ContainsKey(param.Name))
                {
                    object mappedValue = null;

                    mappedValue = OperationMappingHandlers[param.Name].ExecuteMapping(httpRequestMessage, operation);

                    if (mappedValue != null)
                    {
                        Type mappedParameterValueType = mappedValue.GetType();
                        if (!param.ParameterType.IsAssignableFrom(mappedParameterValueType))
                            throw new Exception(string.Format("Mapping handler return type does not match operation input parameter {0}.", param.ParameterType.FullName));
                    }

                    orderedItems.Add(new KeyValuePair<ParameterInfo, object>(param, mappedValue));
                    isFound = true;
                }
                else
                {
                    OperationParameter targetParameter = operation.Parameters.FirstOrDefault(d => d.Name == param.Name);

                    if (targetParameter.IsOptional)
                    {
                        var uriSplit = httpRequestMessage.Uri.Split(new char[] { '?' });
                        string queryString = "";
                        if (uriSplit.Length > 1)
                            queryString = uriSplit[1];

                        foreach (var kvp in queryString.Split(new char[] { '&' }))
                        {
                            string key = "";
                            string value = "";

                            var split = kvp.Split(new char[] { '=' });
                            if (split.Length > 0)
                                key = split[0];
                            if (split.Length > 1)
                            {
                                for (int i = 1; i < split.Length; i++)
                                    if (i == split.Length - 1)
                                        value += split[i];
                                    else
                                        value += split[i] + "=";
                            }

                            // find the key in the request query string (if it exists) for the associated input parameter mapped to query string value.
                            var placeholderValue = GetQuerystringValuePlaceholder(key, operation.UriTemplate);
                            if (placeholderValue == null) placeholderValue = "";
                            // if (param.Name.ToLower().Equals(key.ToLower()))
                            if (param.Name.ToLower().Equals(placeholderValue.ToLower()))
                            {
                                // found query parameter.
                                Type typeToIdentify = param.ParameterType;
                                try
                                {
                                    typeToIdentify = GetParameterType(param);
                                    paramValue = Convert.ChangeType(value, typeToIdentify);
                                }
                                catch (Exception ex)
                                {
                                    throw new UriParameterMalformedException(typeToIdentify, param.Name);
                                }

                                orderedItems.Add(new KeyValuePair<ParameterInfo, object>(param, paramValue));
                                isFound = true;
                            }
                        }

                        if (!isFound)
                        {
                            orderedItems.Add(new KeyValuePair<ParameterInfo, object>(param, GetDefault(param.ParameterType)));
                            isFound = true;
                        }
                    }
                    else
                    {
                        // foreach (var item in httpRequestMessage.RouteValues.Keys)
                        if (httpRequestMessage.RouteInformation != null && httpRequestMessage.RouteInformation.RouteData != null)
                            foreach (var item in httpRequestMessage.RouteInformation.RouteData.Keys)
                            {
                                if (param.Name.ToLower().Equals(item.ToLower()))
                                {
                                    try
                                    {
                                        // paramValue = Convert.ChangeType(httpRequestMessage.RouteValues[item], param.ParameterType);
                                        if (param.ParameterType.FullName == typeof(Guid).FullName)
                                            paramValue = new Guid(httpRequestMessage.RouteInformation.RouteData[item].ToString());
                                        else
                                            paramValue = Convert.ChangeType(httpRequestMessage.RouteInformation.RouteData[item], param.ParameterType);
                                    }
                                    catch (Exception ex)
                                    {
                                        throw new UriParameterMalformedException(param.ParameterType, param.Name);
                                    }

                                    orderedItems.Add(new KeyValuePair<ParameterInfo, object>(param, paramValue));
                                    isFound = true;
                                    break;
                                }
                            }
                    }

                }

                if (!isFound)
                    throw new Exception("Operation value not found for " + param.Name + ".");
            }

            return method.Invoke(serviceInstance, orderedItems.Select(d => d.Value).ToArray());
        }
        
        /// <summary>
        /// Determine the response media type formatter for the associated REST request.
        /// </summary>
        /// <param name="request"></param>
        /// <param name="configuration"></param>
        /// <returns></returns>
        public static IMediaTypeFormatter GetResponseMediaTypeFormatter(IHttpRequestMessage request, Configuration configuration)
        {
            if (request == null)
                throw new Exception("Unable to determine media type formatter based on a null request.");
            else if (configuration == null)
                throw new Exception("Unable to determine media type formatter based on null configuration.");

            IMediaTypeFormatter formatter = null;

            if (configuration.MediaTypeFormatters != null)
            {
                var acceptHeader = request.Headers.FirstOrDefault(d => d.Key.ToLower().Equals("accept"));

                if (acceptHeader != null)
                    foreach (var key in acceptHeader.Value.Split(new char[] { ';', ',' }, StringSplitOptions.RemoveEmptyEntries))
                    {
                        if (key.ToLower().Trim().Equals("*/*"))
                            return configuration.MediaTypeFormatters.First();

                        foreach (var mediaType in configuration.MediaTypeFormatters.Where(d => d.SupportedMediaTypes != null && d.SupportedMediaTypes.Count() > 0))
                        {
                            foreach (var mediaKey in mediaType.SupportedMediaTypes)
                            {
                                if (key.Trim().ToLower().Equals(mediaKey.ToLower()))
                                {
                                    return mediaType;
                                }
                            }
                        }
                    }
                else if (configuration.MediaTypeFormatters != null && acceptHeader == null)
                    return configuration.MediaTypeFormatters.FirstOrDefault();
            }

            return null;
        }
        /// <summary>
        /// Determine the request media type formatter for the associated REST request.
        /// </summary>
        /// <param name="request"></param>
        /// <param name="configuration"></param>
        /// <returns></returns>
        public static IMediaTypeFormatter GetRequestMediaTypeFormatter(IHttpRequestMessage request, Configuration configuration)
        {
            if (request == null)
                throw new Exception("Unable to determine media type formatter based on a null request.");
            else if (configuration == null)
                throw new Exception("Unable to determine media type formatter based on null configuration.");

            IMediaTypeFormatter formatter = null;

            var contentTypeHeader = request.Headers.FirstOrDefault(d => d.Key.ToLower().Equals("content-type"));

            if (configuration.MediaTypeFormatters != null && contentTypeHeader != null)
            {
                foreach (var key in contentTypeHeader.Value.Split(new char[] { ';', ',' }, StringSplitOptions.RemoveEmptyEntries))
                {
                    if (key.ToLower().Trim().Equals("*/*"))
                        return configuration.MediaTypeFormatters.First();

                    foreach (var mediaType in configuration.MediaTypeFormatters.Where(d => d.SupportedMediaTypes != null && d.SupportedMediaTypes.Count() > 0))
                    {
                        foreach (var mediaKey in mediaType.SupportedMediaTypes)
                        {
                            if (key.Trim().ToLower().Equals(mediaKey.ToLower()))
                            {
                                return mediaType;
                            }
                        }
                    }
                }
            }
            else if (configuration.MediaTypeFormatters != null && contentTypeHeader == null)
                return configuration.MediaTypeFormatters.FirstOrDefault();

            return null;
        }

        /// <summary>
        /// Determines if the ky is present in the URI querystring template specified and if so, returns it.
        /// </summary>
        /// <param name="key"></param>
        /// <param name="uriTemplate"></param>
        /// <returns></returns>
        private string GetQuerystringValuePlaceholder(string key, string uriTemplate)
        {
            var split = uriTemplate.Split(new char[] { '?' });
            if (split.Length < 2)
            {
                return null;
            }

            foreach (var kvp in split[1].Split(new char[] { '&' }, StringSplitOptions.RemoveEmptyEntries))
            {
                var kvpSplit = kvp.Split(new char[] { '=' }, StringSplitOptions.RemoveEmptyEntries);
                string kvpKey = "";
                if (kvpSplit.Length > 0)
                    kvpKey = kvpSplit[0];

                if (kvpKey.ToLower().Equals(key.ToLower()))
                {
                    if (kvpSplit.Length > 1)
                        return kvpSplit[1].Replace("{", "").Replace("}", "");
                    else
                        return string.Empty;
                }
            }

            return null;
        }

        /// <summary>
        /// Attempt to resolve parameter type for associated ParameterInfo class.  This is not as simple as simply looking at the type due to nullable values
        /// and other possible valid types that could contain the actual type.
        /// </summary>
        /// <param name="pInfo"></param>
        /// <returns></returns>
        private Type GetParameterType(ParameterInfo pInfo)
        {
            try
            {
                if (pInfo.ParameterType.GetGenericTypeDefinition() == typeof(Nullable<>))
                {
                    return pInfo.ParameterType.GetGenericArguments()[0];
                }
            }
            catch { }

            return pInfo.ParameterType;
        }

        /// <summary>
        /// Return default value for associated type.
        /// </summary>
        /// <param name="type"></param>
        /// <returns></returns>
        private static object GetDefault(Type type)
        {
            if (type.IsValueType)
            {
                return Activator.CreateInstance(type);
            }
            return null;
        }

        /// <summary>
        /// Get specified attribute from target parameter info.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="type"></param>
        /// <returns></returns>
        private T GetAttribute<T>(ParameterInfo type) where T : Attribute
        {
            if (type == null)
                return null;

            foreach (var attribute in type.GetCustomAttributes(true))
                if (attribute.GetType().FullName == typeof(T).FullName)
                    return (T)Convert.ChangeType(attribute, typeof(T));

            return null;
        }
    }
}
